"""
This file contains replaced methods for QuerySet.
"""

from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db.models.query import QuerySet, Q
import qshash

# Python 2.3 compatibility
if not hasattr(__builtins__, 'set'):
    from sets import Set as set

class CachingInfo(object):
    """
    This P.O.D. class just contains attributes used by the CachingQuerySet.
    """
    def __init__(self):
        self.individual = False
        self.queryset_key = ''
        self.relations = set()
        self.format = 'qscache:%d,%d' # % (contenttype, pk)
        self.timeout = None
        # TODO: Include more data about a given QuerySet (at least for
        # debugging) so that QuerySets that don't actually match perfectly
        # are rejected
    
    def clone(self):
        """Returns a deep copy of this object."""
        c = CachingInfo()
        c.individual = self.individual
        c.queryset_key = self.queryset_key
        c.relations = self.relations.copy()
        c.format = self.format
        c.timeout = self.timeout
        return c

#############################
# REPLACED QUERYSET METHODS #
#############################

def __init__(self, model=None):
    self.model = model
    self._filters = Q()
    self._order_by = None         # Ordering, e.g. ('date', '-name').
                                  # If None, use model's ordering.
    self._select_related = False  # Whether to fill cache for related
                                  # objects.
    self._max_related_depth = 0   # Maximum "depth" for select_related
    self._distinct = False        # Whether the query should use SELECT
                                  # DISTINCT.
    self._select = {}             # Dictionary of attname -> SQL.
    self._where = []              # List of extra WHERE clauses to use.
    self._params = []             # List of params to use for extra WHERE
                                  # clauses.
    self._tables = []             # List of extra tables to use.
    self._offset = None           # OFFSET clause.
    self._limit = None            # LIMIT clause.
    self._result_cache = None

    # New attributes
    self._qscache = CachingInfo() # Data specifying how to cache the QS.
    self._hash = 0                # Return value of hash().

def _clone(self, klass=None, **kwargs):
    if klass is None:
        klass = self.__class__
    c = klass()
    c.model = self.model
    c._filters = self._filters
    c._order_by = self._order_by
    c._select_related = self._select_related
    c._max_related_depth = self._max_related_depth
    c._distinct = self._distinct
    c._select = self._select.copy()
    c._where = self._where[:]
    c._params = self._params[:]
    c._tables = self._tables[:]
    c._offset = self._offset
    c._limit = self._limit
    # Caching data
    c._qscache = self._qscache.clone() # Too much redundant copying?
    c._hash = self._hash
    c.__dict__.update(kwargs)
    return c

def __hash__(self):
    # offset and limit are awkward to catch, so just do them here:
    return self._hash | (self._offset is not None and qshash.OFFSET) \
                      | (self._limit is not None and qshash.LIMIT)

def extra(self, select=None, where=None, params=None, tables=None):
    assert self._limit is None and self._offset is None, \
            "Cannot change a query once a slice has been taken"
    clone = self._clone()
    if select:
        clone._select.update(select)
        clone._hash |= qshash.SELECT_EXTRA
    if where:
        clone._where.extend(where)
        clone._hash |= qshash.WHERE_EXTRA
    if params: clone._params.extend(params)
    if tables: clone._tables.extend(tables)
    return clone

#######################
# METHOD REPLACEMENTS #
#######################

QuerySet.extra = extra
QuerySet._clone = _clone
QuerySet.__hash__ = __hash__
QuerySet.__init__ = __init__

# Add hash-setting code to trivial QS methods with a decorator.
def set_flag(flag, true_or_false_default=None):
    """Sets the given flag on the clone returned by the decorated QS method."""
    def decorate(func):
        if true_or_false_default is not None:
            # The QuerySet method takes a true_or_false; set the flag with it.
            def decorated(self, cond=true_or_false_default, *args, **kwargs):
                c = func(self, cond, *args, **kwargs)
                c._hash = cond and (c._hash | flag) or (c._hash & ~flag)
                return c
        else:
            # Normal QuerySet method. Just set the flag no matter what
            def decorated(self, *args, **kwargs):
                clone = func(self, *args, **kwargs)
                clone._hash |= flag
                return clone
        return decorated
    return decorate

QuerySet.values = set_flag(qshash.VALUES)(QuerySet.values)
QuerySet.dates = set_flag(qshash.DATES)(QuerySet.dates)
QuerySet.order_by = set_flag(qshash.ORDER_BY)(QuerySet.order_by)
# true_or_false cases:
QuerySet.distinct = set_flag(qshash.DISTINCT, True)(QuerySet.distinct)
QuerySet.select_related = set_flag(qshash.SELECT_RELATED, True)(
        QuerySet.select_related)

